<head>
<title>Playback tweets from a query</title>
<style>
*{
margin:0;
padding:0;
}
a{
text-decoration:none;
color:#0084b4;
}
a:hover{
text-decoration:underline;
}
label{
float: left;
width: 120px;
font-weight: bold;
}

input, textarea{
width: 180px;
margin-bottom: 5px;
}

textarea{
width: 250px;
height: 150px;
}

#submitbutton{
margin-left: 120px;
margin-top: 5px;
width: 90px;
}

form br{
clear: left;
}

#statusmsg { margin: 15px 0 }
body{
text-align:center;
font:.75em 'Lucida Grande',sans-serif;
color:#333;
}
#container{
width:763px;
margin:1em auto;
text-align:left;
position:relative;
}
#content{
background-color:#FFF;
width:564px;
margin-top:0 word-wrap:break-word;
-moz-border-radius-topleft:5px;
-webkit-border-top-left-radius:5px;
-moz-border-radius-bottomleft:5px;
-webkit-border-bottom-left-radius:5px;
}
#sidebar {
width: 180px;
float: right;
color: #5C73B8;
margin: 12px;
font-weight: bold;
}
#sidebar a {
color: #5C73B8;
text-decoration:underline;
}
#sidebar a:hover {
color: #3dc;
text-decoration:underline;
}

#sidebar p {
margin-bottom: 8px;
}
#twitter_data {
margin-top: 18px;
}
.date{
font-size:10px;
color: #666;
}
.date a {
font-size:10px;
color:#666;
}
#about {
position: absolute;
margin-top: 20px; 
margin-left: 0px;
width: 640px;
background: #f9f9f9;
border: 1px solid #CC9933;
padding: .6em;
word-wrap:break-word;
overflow:hidden;
line-height:1.2;
font-size:1.35em;
opacity: 0.85;
z-index: 1000; 
}
#about p {
margin-bottom: 2.0em;
}
#snd_btn { margin-top: 15px; }
</style>

<script language="Javascript"> 
String.prototype.parseURL = function() {
	return this.replace(/[A-Za-z]+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+/, function(url) {
		return url.link(url);
	});
};
String.prototype.parseUsername = function() {
	return this.replace(/[@]+[A-Za-z0-9-_]+/, function(u) {
		var username = u.replace("@","")
		return u.link("http://twitter.com/"+username);
	});
};
String.prototype.parseHashtag = function() {
	return this.replace(/[#]+[A-Za-z0-9-_]+/, function(t) {
		var tag = t.replace("#","%23")
		return t.link("http://search.twitter.com/search?q="+tag);
	});
};

var q, start_date, start_date_str;
var start_reached = false;
var statusmsg_elem_id = 'statusmsg';
var statusmsg_el;
var result_elem_id = 'twitter_data';

var Tweets = new Array();
var TweetQueue = new Array();

var debug = false;
var page = 0;
// maximum search will return is 100 per page and 1500 total
var per_page = 100;
var max_pages = 15;
// number of searches to loop, be careful of rate limits
var max_searches = 4;
var querycnt = 0;
var qindex;
var rt_cursor;
var pb_cursor;
var the_timer;
var cb_timer;

var lastObj;


var playbackModes = [{
 status: "Paused",
 rate: 0.0
},{
 status: "Normal",
 rate: 1.0
},{
 status: "1 &gt;",
 rate: 3.0,
 audioUrl: "nav_in.wav"
},{
 status: "2 &gt;&gt;",
 rate: 20.0,
 audioUrl: "nav_in.wav"
},{
 status: "3 &gt;&gt;&gt;",
 rate: 60.0,
 audioUrl: "nav_in.wav"
}];

var playback_mode;
var sound = true;

function tweet(from_user, text, created_at, id, profile_image_url) {
  var x = new Date(created_at);
  this.from_user = from_user;
  this.text = text;
  this.created_at = x;
  this.id = id;
  this.profile_image_url = profile_image_url;
}
function getQueryVariable(variable) {
  var query = window.location.search.substring(1);
  var vars = query.split("&");
  for (var i=0;i<vars.length;i++) {
    var pair = vars[i].split("=");
    if (pair[0] == variable) {
      return pair[1];
    }
  }
  return "";
} 
  
function sortByDate(a, b) {
  var x = a.created_at.getTime();
  var y = b.created_at.getTime();
  return ((x < y) ? 1 : ((x > y) ? -1 : 0));
}

function sortByRevDate(a, b) {
  return sortByDate(b, a);
}

function show_tweets() {
    var html = "";
    Tweets.sort(sortByDate);
    var len=Tweets.length;
    if (len > per_page)
      len = per_page;
    html = "<table>";
    for  ( var i=0; i<len; ++i) {
      var thisTweet = Tweets[i];
     html += "<tr><td width='54' valign='top'><img src='" + thisTweet.profile_image_url 
         + "' width='48' height='48'></td><td valign='top'><a href='http://twitter.com/"
         + thisTweet.from_user + "'>" + thisTweet.from_user + "</a>: "
         + thisTweet.text.parseURL().parseUsername().parseHashtag() + "<br/>" 
         + "<i><a href='http://twitter.com/"
         + thisTweet.from_user + "/status/"
         + thisTweet.id + "' class='date'>" + thisTweet.created_at.toString() 
         + "</a></i></td></tr><tr height='12'><td colspan='2'>&nbsp;</td</tr>";
    }
    html += "</table>";
    document.getElementById(result_elem_id).innerHTML = html;
}

function check_play() {
  clearTimeout(the_timer);
  if (playback_mode == 0)
    return false;
  if (qindex >= TweetQueue.length) {
    statusmsg_el.innerHTML += "<p>Reached end of Tweets</p>";
  } else {
    var now = new Date();
    var delta = now.getTime() - rt_cursor.getTime();
    rt_cursor = now;
    pb_cursor.setTime(pb_cursor.getTime()+delta*playbackModes[playback_mode].rate);
    document.getElementById("the_time_is").innerHTML = "<p>" + pb_cursor.toString() + "</p>";
    var d = new Date(pb_cursor.getTime() - start_date.getTime());
    var dur_hr = d.getUTCHours();
    var dur_min = d.getUTCMinutes();
    dur_min = dur_min + "";   
    if (dur_min.length == 1) {
      dur_min = "0" + dur_min;
    }
    var dur_sec = d.getUTCSeconds();
    dur_sec = dur_sec + "";   
    if (dur_sec.length == 1) {
      dur_sec = "0" + dur_sec;
    }
    document.getElementById('pb_cursor').innerHTML = "<p>Cursor: " + dur_hr + ":" + dur_min + ":" + dur_sec + "</p>";
    var next_event = TweetQueue[qindex].created_at.getTime() - pb_cursor.getTime();
    if (next_event <= 0) {
      Tweets[Tweets.length] = TweetQueue[qindex];
      qindex++;
      return true;
    } else {
      if (debug) statusmsg_el.innerHTML = "<p>next event in " + next_event + "ms (" + TweetQueue[qindex].created_at.toString() + ")</p>";
      if (next_event > 1000)
        next_event = 1000;
      the_timer = setTimeout("process_events();", next_event);
    }
  }
  return false;
}

function process_events() {
  var xn = Tweets.length;
  while (check_play()) 
    ;
  if (Tweets.length > xn) {
    show_tweets();
    if (sound && tweet_snd) {
      try {
        tweet_snd.currentTime = 0;
        tweet_snd.play();
      } catch (e) {
        set_sound(false);
        delete window.tweet_snd;
      }
    }
  }
}

function setPlaybackMode(m) {
  if (playback_mode == 0) {
    rt_cursor = new Date();
  }
  playback_mode = m;
  if (playbackModes[playback_mode].snd) {
    try {
      playbackModes[playback_mode].snd.currentTime = 0;
      playbackModes[playback_mode].snd.play();
    } catch (e) {
      delete playbackModes[playback_mode].audioUrl;
      delete playbackModes[playback_mode].snd;
    }
  }
  document.getElementById('pb_status').innerHTML = "<p>Playback: " + playbackModes[playback_mode].status + "</p>";
  process_events();  
}

function fast_forward() {
  if (playback_mode < playbackModes.length-1)
    setPlaybackMode(playback_mode+1);
}

function toggle_sound() {
  set_sound(!sound);
}

function set_sound(v) {
  sound = v;
  if (sound) {
    document.getElementById('snd_btn').innerHTML = "Sound Off";
  } else {
    document.getElementById('snd_btn').innerHTML = "Sound On";
  }
}

function start_playback() {
  statusmsg_el.innerHTML = "";
  TweetQueue.sort(sortByRevDate);  
  try {
    tweet_snd = new Audio("cuckoo_clock1_x.wav");
  } catch (e) {
    set_sound(false);
  }
  for (var i = 0; i < playbackModes.length; i++) {
    if (playbackModes[i].audioUrl) {
      try {
        playbackModes[i].snd = new Audio(playbackModes[i].audioUrl);
      } catch (e) {
        delete playbackModes[i].audioUrl;
        delete playbackModes[i].snd;
      }
    }
  }
  rt_cursor = new Date();
  pb_cursor = new Date(start_date.getTime());
  playback_mode = 1;
  qindex = 0;
  document.getElementById('pb_controls').style.display = 'block';
  setPlaybackMode(1);
}

function callback_failed() {
  statusmsg_el.innerHTML += "<h1 class='error'>Twitter API failed!</h1>";
  statusmsg_el.innerHTML += "<p>Check your parameters and try again.</p>";
}

function searchCallback(data) {
  clearTimeout(cb_timer);
  page++;
  statusmsg_el.innerHTML += "<br>processing page " + page;
  lastObj = data;
  var allResults = data['results'];
  if (allResults) {
    statusmsg_el.innerHTML += "<br/>scanning " + allResults.length + " tweets from page " + page + "...";
    for (var resultIndex in allResults) {
      var result = allResults[resultIndex];
      var t = result['text'];
      //lastObj = result;
      var x = new Date(result['created_at']);
      if (x.getTime() < start_date.getTime()) {
        start_reached = true;
      } else {
        if (debug) statusmsg_el.innerHTML += "<br/>adding tweet '" + result['from_user'] + ": " + t + "'";
        TweetQueue[TweetQueue.length] = new tweet(result['from_user'], result['text'], result['created_at'], result['id_str'], result['profile_image_url'])
      }
    }
    if (start_reached) {
      statusmsg_el.innerHTML = 
        "<p>Done! " + TweetQueue.length + " Tweets. <a href='javascript:start_playback()'>click to start playback</a></p>"
    } else {
      if (page < max_pages) {
        var next_page = page + 1;
        statusmsg_el.innerHTML += "<p>Fetching Page: " + next_page + " oldest tweet so far: " + TweetQueue[TweetQueue.length-1].created_at.toString() + " id: " + TweetQueue[TweetQueue.length-1].id + "</p>";
        if (data['next_page']) {
          var url = 'http://search.twitter.com/search.json' + data['next_page'];
          url += '&callback=searchCallback';
          var script = document.createElement('script');
          script.setAttribute('src', url);
          cb_timer = setTimeout("callback_failed();", 15000);
          document.body.appendChild(script);
          return;
        } else {
          statusmsg_el.innerHTML += "<p>Nothing left to search.</p>";
        }
      }
      if (querycnt < max_searches) {
        TweetQueue.sort(sortByRevDate);
        max_id = TweetQueue[0].id;
        querycnt++;
        page = 0;
        statusmsg_el.innerHTML += "<p>New query: " + querycnt + " max_id=" + max_id + "</p>";
        var url = 'http://search.twitter.com/search.json?q='
          + encodeURIComponent(q)
          + '&max_id=' + max_id
          + '&rpp=100&callback=searchCallback';
        var script = document.createElement('script');
        script.setAttribute('src', url);
        cb_timer = setTimeout("callback_failed();", 15000);
        document.body.appendChild(script);
      } else {
        statusmsg_el.innerHTML += "<p>Max queries reached.</p>";
      }
    }
  } 
}

function open_about() {
  document.getElementById(result_elem_id).style.display = 'none';
  document.getElementById('about').style.display = 'block';
}
function close_about() {
  document.getElementById(result_elem_id).style.display = 'block';
  document.getElementById('about').style.display = 'none';
}


window.onload = function() {
  statusmsg_el = document.getElementById(statusmsg_elem_id);
  q = getQueryVariable("q").replace(/\+/g, " ");
  max_id = getQueryVariable("max_id");
  start_date_str = getQueryVariable("start_date").replace(/\+/g, " ");
  if (q.length && start_date_str.length) {
    q = decodeURIComponent(q);
    start_date_str = decodeURIComponent(start_date_str);
    start_date = new Date(start_date_str);
    statusmsg_el.innerHTML = "Retrieving tweets of query " + q + " starting " +  start_date.toString() + " (" + start_date_str + ") ...";
    var url = 'http://search.twitter.com/search.json?q='
  	+ encodeURIComponent(q);
    if (max_id.length) {
        url += '&max_id=' + max_id;
    }
    url += '&rpp=100&callback=searchCallback';
    var script = document.createElement('script');
    script.setAttribute('src', url);
    cb_timer = setTimeout("callback_failed();", 15000);
    document.body.appendChild(script);
    querycnt++;
  } else {
    statusmsg_el.innerHTML = "Please provide Twitter search query and start date";
    var html = "";
    html = "<form><label for='q'>Query:</label>";
    html += "<input type='text' name='q' size='30'><br/>";
    html += "<label for='start_date'>Start date/time: <small>e.g. April 12, 2012 1:30 pm</small></label>";
    html += "<input type='text' name='start_date' size='30'><br/>";
    html += "<input type='submit' value='Go' id='submitbutton'></form>";
    document.getElementById(result_elem_id).innerHTML = html;
  }
}
</script>

</head>
<body>
<div id="container" class="subpage">
<h2>playback tweets from a query</h2>

<div id="sidebar">
<p>Powered by @<a href="http://twitter.com/mrblog" title="Propeller head at large">mrblog</a></p>
<p><a href="javascript:open_about();">About</a></p>

<div id="the_time_is">
</div>

<div id="pb_status">
</div>

<div id="pb_cursor">
</div>

<div id="pb_controls" style="display: none;">
<button onclick="javascript:setPlaybackMode(0);">Pause</button>
<button onclick="javascript:setPlaybackMode(1);">Play</button>
<button onclick="javascript:fast_forward();">FF &gt;&gt;</button>
<button id="snd_btn" onclick="javascript:toggle_sound();">Sound Off</button>
</div>
 
<div id="statusmsg">
<em>Starting...</em>
</div>

</div>

<div id="content">


<div id="twitter_data">
<em>If you don't see Tweets here, something is broken...</em>
</div>

</div>

<div id="about" style="display: none;">
<h2>About</h2>
<p>This tool plays back tweets from a Twitter Search Query starting at
a specified date/time. This tool was inspired by an idea by Brian Ladner
<a href="http://twitter.com/mysterylectric">@mysterylectric</a>
AKA
<a href="http://twitter.com/nascaranonymous">@nascaranonymous</a>
</p>

<p>This tool is entirely in Javascript that runs on your browser.
There is no server-side to it at all.  You can "View Source" and
save as a file to the local hard drive on your computer and 
open it in your browser and edit or modify the script in any way
you like.</p>

<p>This tool works in modern browsers, including Firefox, Chrome, and
Safari. It doesn't work in Internet Explorer (IE). If you want to fix 
that, get the source and become part of the project.</p>

<p><strong>TODO:</strong> There isn't much styling here. This is
somewhat intentional to keep it simple and easy to customize.
Obviously there is lots of room for cleaning up the look with nifty CSS. 
Likewise, jQuery is not used and this is all self-contained HTML, 
CSS, and Javascript. Again, that's intentional, to serve as a baseline
for customization rather than a finished product.
The tweet list is currently a TABLE (heaven forbid) which is of
course against all the modern CSS rules. It should probably be a
styled list.</p>

<p>It's open-source. Download and contribute to the code at:
<a href="http://code.google.com/p/twitterdvr/">http://code.google.com/p/twitterdvr/</a>.</p>

<p align="right"><a href="javascript:close_about();">close</a></p>
</div>

</div>
</body>
